#!/usr/bin/python

"""Virtual filesystem for aggregating music libraries.

Filesystem structure:

/ctl - contains writable files for sending events to mfuser
    /log - symlink to mfuser.log
    /sources - contains symlinks to music libraries outside the filesystem
    /stats - read this file to view a summary of the DB

/artists - contains a subdirectory for each artist in the DB
        /<ARTIST> - contains a subdirectory for each album by that artist
                 /<ALBUM> - contains a file for each track in the album
                         /<TRACK> - actual songs
"""

from __future__ import with_statement

import errno
import logging
import os
import Queue
import re
import sqlite3
import stat
import StringIO
import thread
import threading
import time

# Dependencies!
import fuse
from mutagen import easyid3, mp4, oggvorbis
import pyinotify

fuse.fuse_python_api = (0, 2)

TAGGERS = dict(
  mp3=easyid3,
  ogg=oggvorbis,
  aac=mp4,
)

TAGS = dict(
  track='tracknumber',
  title='title',
  artist='artist',
  album='album',
)

AAC_FIELDS = {
  '\xa9alb': 'album',
  '\xa9nam': 'title',
  '\xa9ART': 'artist',
  'trkn': 'tracknumber',
}

SUPPORTED_TAG_ENCODINGS = ['utf-8', 'latin1']
TRACK_PATTERN = re.compile(r'\d+')
FILENAME_PATTERN = re.compile(r'^(?P<track>\d\d)?( - )?(?P<title>.*)$')

_music_factory = None


def GetMusic():
  global _music_factory
  if not _music_factory:
    _music_factory = MusicDBFactory()
  return _music_factory.GetMusicDB()


def musicdb(method):
  """Decorator for creating/opening the music DB and passing it along."""
  def stub(fs, *args, **kwargs):
    return method(fs, GetMusic(), *args, **kwargs)
  return stub


class Stat(fuse.Stat):

  def __init__(self, **kwargs):
    self.st_mode = kwargs.get('st_mode', 0)
    self.st_ino = kwargs.get('st_ino', 0)
    self.st_dev = kwargs.get('st_dev', 0)
    self.st_nlink = kwargs.get('st_nlink', 0)
    self.st_uid = kwargs.get('st_uid', os.getuid())
    self.st_gid = kwargs.get('st_gid', os.getgid())
    self.st_size = kwargs.get('st_size', 0)
    self.st_atime = kwargs.get('st_atime', int(time.time()))
    self.st_mtime = kwargs.get('st_mtime', int(time.time()))
    self.st_ctime = kwargs.get('st_ctime', int(time.time()))


class VfsNode(object):

  def __init__(self, name):
    self.name = name

  def Stat(self):
    raise NotImplementedError


class VfsDir(VfsNode):

  def __init__(self, name, entries=None, entry_generator=None):
    VfsNode.__init__(self, name)
    self.entries = entries
    self.entry_generator = entry_generator

  def Resolve(self, path_elems):
    if not path_elems:
      raise IOError(errno.ENOENT)
    if len(path_elems) == 1 and not path_elems[0]:
      return self
    if self.entries:
      entries = self.entries
    else:
      entries = self.entry_generator()
    matches = [entry for entry in entries if entry.name == path_elems[0]]
    if matches:
      if len(path_elems) == 1:
        return matches[0]
      elif not isinstance(matches[0], VfsDir):
        raise IOError(errno.ENOTDIR)
      else:
        return matches[0].Resolve(path_elems[1:])
    else:
      raise IOError(errno.ENOENT)

  def Stat(self):
    if self.entries:
      entries = self.entries
    else:
      entries = self.entry_generator()
    entry_count = len([entry for entry in entries if isinstance(entry, VfsDir)])
    return Stat(st_mode=(stat.S_IFDIR | 0755), st_nlink=(entry_count + 3))

  def ReadDir(self):
    entries = []
    if self.entries:
      entries.extend(self.entries)
    else:
      entries.extend(self.entry_generator())
    entries = [fuse.Direntry(EncodePathElement(entry.name)) for entry in entries]
    entries.extend([fuse.Direntry('.'), fuse.Direntry('..')])
    return entries

  def MakeLink(self, name, target):
    raise IOError(errno.EPERM)

  def Unlink(self, name):
    raise IOError(errno.EPERM)


class VfsLink(VfsNode):

  def __init__(self, name, target):
    VfsNode.__init__(self, name)
    self.target = target

  def Stat(self):
    return Stat(st_mode=(stat.S_IFLNK | 0755), st_nlink=1)

  def ReadLink(self):
    return self.target


class VfsFile(VfsNode):

  def __init__(self, name, real_path=None, readonly_content=None, readonly_content_generator=None):
    VfsNode.__init__(self, name)
    self.real_path = real_path
    self.readonly_content = readonly_content

  def Stat(self):
    if self.real_path:
      return os.stat(self.real_path)
    else:
      return Stat(st_mode=(stat.S_IFREG | 0644), st_nlink=1)

  def GetReader(self):
    if self.real_path:
      return VfsFdReader(os.open(self.real_path, os.O_RDONLY))
    elif self.readonly_content is not None:
      return VfsStringReader(self.readonly_content)
    else:
      return VfsStringReader(self.readonly_content_generator())


class VfsReader(object):

  def Read(self, length, offset):
    raise NotImplementedError


class VfsFdReader(VfsReader):

  def __init__(self, fd):
    self.fd = fd

  def Read(self, length, offset):
    os.lseek(self.fd, offset, os.SEEK_SET)
    return os.read(self.fd, length)


class VfsStringReader(VfsReader):

  def __init__(self, content):
    self.content = content

  def Read(self, length, offset):
    return self.content[offset:(offset + length)]


class MusicVfs(VfsDir):

  def __init__(self, scanner):
    VfsDir.__init__(self, 'mfuser',
                    entries=[
                        CtlVfs(scanner),
                        ArtistListVfs(),
                    ])


class CtlVfs(VfsDir):

  def __init__(self, scanner):
    VfsDir.__init__(self, 'ctl',
                    entries=[
                      LogVfs(),
                      SourcesVfs(scanner),
                      StatsVfs(),
                    ])


class LogVfs(VfsLink):

  def __init__(self):
    VfsLink.__init__(self, 'log', logging_path)


class SourcesVfs(VfsDir):

  def __init__(self, scanner):
    VfsDir.__init__(self, 'sources', entry_generator=self.Entries)
    self.scanner = scanner

  def Entries(self):
    return [VfsLink(name, path) for name, path in GetMusic().GetSources()]

  def AddLink(self, name, target):
    GetMusic().AddSource(name, os.path.abspath(target))
    self.scanner.AddJob(AddDirectoryJob(name, os.path.abspath(target)))

  def Unlink(self, name):
    if not GetMusic().RemoveSource(name):
      raise IOError(errno.ENOENT)


class StatsVfs(VfsFile):

  def __init__(self):
    stats = GetMusic().GetStats()
    keys = sorted(stats.keys())
    content = '\n'.join('%s: %s' % (k, stats[k]) for k in keys) + '\n'
    VfsFile.__init__(self, 'stats', readonly_content=content)


class ArtistListVfs(VfsDir):

  def __init__(self):
    VfsDir.__init__(self, 'artists', entry_generator=self.Entries)

  def Entries(self):
    return [ArtistVfs(artist) for artist in GetMusic().GetArtists()]


class ArtistVfs(VfsDir):

  def __init__(self, artist):
    VfsDir.__init__(self, artist, entry_generator=self.Entries)
    self.artist = artist

  def Entries(self):
    return [AlbumVfs(self.artist, album)
            for album in GetMusic().GetAlbums(self.artist)]


class AlbumVfs(VfsDir):

  def __init__(self, artist, album):
    VfsDir.__init__(self, album, entry_generator=self.Entries)
    self.artist = artist
    self.album = album

  def Entries(self):
    return [TrackVfs(track, title, media_type, filename)
            for track, title, media_type, filename
            in GetMusic().GetTracks(self.artist, self.album)]


class TrackVfs(VfsFile):

  def __init__(self, track, title, media_type, filename):
    self.track = track
    self.title = title
    self.media_type = media_type
    self.filename = filename
    VfsFile.__init__(self, self.BuildFilename(), real_path=filename)

  def BuildFilename(self):
    """Construct a track filename from field data in the DB."""
    if self.title:
      if self.track is not None:
        return u'%02d - %s.%s' % (self.track, self.title.replace('/', '%2F'), self.media_type)
    return os.path.basename(self.filename)


def EncodePathElement(value):
  """Convert unicode to utf-8 encoded bytes and escape slashes."""
  return value.replace('/', '%2F').encode('utf-8')


def DecodePathElement(value):
  """Convert a directory entry to unicode (assuming utf-8)."""
  return value.decode('utf-8').replace('%2F', '/')


def DecodePath(path):
  """Split and decode a path into unicode."""
  if path.startswith('/'):
    path = path[1:]
  return [DecodePathElement(elem) for elem in path.split('/')]


class MusicFile(object):
  """Support opening/reading/writing ctl and track files."""

  def __init__(self, vfs, path, flags, mode=None):
    logging.debug('open: %s, %04o', path, flags)
    mask = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
    self.direct_io = True
    node = vfs.Resolve(DecodePath(path))
    if not isinstance(node, VfsFile) or (flags & mask) != os.O_RDONLY:
      raise IOError(errno.EACCES)
    self.reader = node.GetReader()

  def read(self, length, offset):
    return self.reader.Read(length, offset)


class MusicFS(fuse.Fuse):
  """Filesystem-based interface to a music DB."""

  def __init__(self, *args, **kwargs):
    fuse.Fuse.__init__(self, *args, **kwargs)
    self.scanner = ScanDriver()
    self.scanner.start()
    self.vfs = MusicVfs(self.scanner)

    class VfsMusicFile(MusicFile):

      def __init__(mf, path, flags, mode=None):
        MusicFile.__init__(mf, self.vfs, path, flags, mode)

    self.file_class = VfsMusicFile

  def main(self, *args, **kwargs):
    fuse.Fuse.main(self, *args, **kwargs)
    self.scanner.Stop()

  def getattr(self, path):
    try:
      node = self.vfs.Resolve(DecodePath(path))
    except IOError, e:
      return -e.message
    stat = node.Stat()
    return stat

  def readdir(self, path, offset):
    node = self.vfs.Resolve(DecodePath(path))
    if not isinstance(node, VfsDir):
      raise IOError(errno.ENOTDIR)
    return node.ReadDir()

  def readlink(self, path):
    node = self.vfs.Resolve(DecodePath(path))
    if not isinstance(node, VfsLink):
      raise IOError(errno.EINVAL)
    return str(node.ReadLink())

  def symlink(self, target, name):
    path_elems = DecodePath(name)
    node = self.vfs.Resolve(path_elems[:-1])
    if not isinstance(node, VfsDir):
      raise IOError(errno.ENOTDIR)
    node.AddLink(path_elems[-1], target)

  def unlink(self, name):
    path_elems = DecodePath(name)
    node = self.vfs.Resolve(path_elems[:-1])
    if not isinstance(node, VfsDir):
      raise IOError(errno.ENOTDIR)
    node.Unlink(path_elems[-1])


class MusicDBFactory(object):
  """Factory for opening and caching DB connections."""

  def __init__(self, db_path=None):
    self.cache = {}
    self.lock = threading.Lock()
    self.db_path = db_path or self.GetDefaultDbPath()

  def GetMusicDB(self):
    self.lock.acquire()
    try:
      if thread.get_ident() not in self.cache:
        self.cache[thread.get_ident()] = {}
      if self.db_path not in self.cache[thread.get_ident()]:
        self.cache[thread.get_ident()][self.db_path] = MusicDB(self.db_path)
      return self.cache[thread.get_ident()][self.db_path]
    finally:
      self.lock.release()

  def GetDefaultDbPath(self):
    db_path = os.getenv('MFUSER_DB', '~/.mfuser/music.sqlite')
    if db_path.startswith('~/'):
      db_path = os.path.join(os.getenv('HOME', '/'), db_path[2:])
    return db_path


def transaction(method):
  def RunInTransaction(db_holder, *args, **kwargs):
    try:
      result = method(db_holder, *args, **kwargs)
    except:
      db_holder.db.rollback()
      raise
    else:
      db_holder.db.commit()
      return result
  return RunInTransaction


class MusicDB(object):
  """Interface to the music DB."""

  SCHEMA = {
    'Sources': '(name TEXT NOT NULL PRIMARY KEY, path TEXT NOT NULL)',
    'Files': ('(source TEXT NOT NULL,'
              ' path TEXT NOT NULL,'
              ' media_type TEXT NOT NULL,'
              ' artist TEXT NOT NULL,'
              ' album TEXT NOT NULL,'
              ' title TEXT,'
              ' track INTEGER,'
              ' size INTEGER NOT NULL,'
              ' updated INTEGER NOT NULL,'
              ' UNIQUE(source, path))'),
  }

  UNKNOWN_ARTIST = 'Unknown Artist'
  UNKNOWN_ALBUM = 'Unknown Album'

  def __init__(self, db_path):
    self.db_path = db_path
    db_dir = os.path.dirname(self.db_path)
    if not os.path.exists(db_dir):
      logging.info("Creating music DB directory: %s", db_dir)
      os.makedirs(db_dir)
    self.db = sqlite3.connect(self.db_path)
    self.CheckSchema()

  @transaction
  def CheckSchema(self):
    for table_name, sql in self.SCHEMA.iteritems():
      self.db.execute('CREATE TABLE IF NOT EXISTS %s %s' % (table_name, sql))
      self.db.commit()

  @transaction
  def GetSourceNames(self):
    return [row[0] for row in self.GetSources()]

  @transaction
  def GetSources(self):
    cursor = self.db.cursor()
    cursor.execute('SELECT name, path FROM Sources')
    return cursor.fetchall()

  @transaction
  def AddSource(self, name, path):
    self.db.execute('INSERT INTO Sources VALUES (?, ?)', (name, path))

  @transaction
  def HasSource(self, name):
    for row in self.db.execute('SELECT COUNT(*) FROM Sources WHERE name = ?', (name,)):
      return row[0]

  @transaction
  def GetSource(self, name):
    for row in self.db.execute('SELECT path FROM Sources WHERE name = ?', (name,)):
      return row[0]
    else:
      return None

  @transaction
  def RemoveSource(self, name):
    cursor = self.db.cursor()
    cursor.execute('DELETE FROM Sources WHERE name = ?', (name,))
    if not cursor.rowcount:
      return False
    cursor.execute('DELETE FROM Files WHERE source = ?', (name,))
    return True

  @transaction
  def CheckFile(self, path, fields):
    self.CheckFileWithinTransaction(path, fields)

  def CheckFileWithinTransaction(self, path, fields):
    logging.debug('Considering file for import: %s', path)
    path = DecodeTagValue(os.path.abspath(path))
    for row in self.db.execute('SELECT name, path FROM Sources'):
      base = row[1] + '/'
      if path.startswith(base):
        logging.debug('  Tagged: %s', fields)
        logging.info('Adding to DB: %s', path)
        self.AddFileWithinTransaction(row[0], path, **fields)
        return
    logging.debug('Ignoring file: %s', path)

  @transaction
  def AddFile(self, source, path, size, media_type, artist=None, album=None, title=None, track=None):
    self.AddFileWithinTransaction(source, path, size, media_type, artist, album, title, track)
    
  def AddFileWithinTransaction(self, source, path, size, media_type, artist=None, album=None, title=None, track=None):
    if artist is None:
      artist = self.UNKNOWN_ARTIST
    if album is None:
      album = self.UNKNOWN_ALBUM
    if title is None:
      title, ext = os.path.splitext(os.path.basename(path))
      if ext and ext[1:] != media_type:
        title = os.path.basename(path)
    self.db.execute(
        'REPLACE INTO Files (source, path, size, media_type,'
        ' artist, album, title, track, updated)'
        ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
        (source, path, size, media_type, artist, album, title, track, int(time.time())))

  @transaction
  def RemoveFilesUnder(self, source, prefix):
    cursor = self.db.cursor()
    cursor.execute('DELETE FROM Files WHERE source = ?'
                   ' AND SUBSTR(path, 0, ?) = ?',
                   (source, len(prefix), prefix))
    return cursor.rowcount

  @transaction
  def GetArtists(self):
    cursor = self.db.cursor()
    cursor.execute('SELECT DISTINCT artist FROM Files')
    return [row[0] for row in cursor.fetchall() if row[0]]

  @transaction
  def HasArtist(self, artist):
    cursor = self.db.cursor()
    cursor.execute('SELECT COUNT(*) FROM Files WHERE artist = ?', (artist,))
    return [row[0] for row in cursor.fetchall() if row[0]]

  @transaction
  def GetAlbums(self, artist):
    cursor = self.db.cursor()
    cursor.execute('SELECT DISTINCT album FROM Files WHERE artist = ?', (artist,))
    return [row[0] for row in cursor.fetchall() if row[0]]

  @transaction
  def HasAlbum(self, artist, album):
    cursor = self.db.cursor()
    cursor.execute('SELECT COUNT(*) FROM Files WHERE artist = ? AND album = ?', (artist, album))
    return cursor.fetchone()[0]

  @transaction
  def GetTracks(self, artist, album):
    cursor = self.db.cursor()
    cursor.execute('SELECT track, title, media_type, MAX(path) FROM Files'
                   ' WHERE artist = ? AND album = ? GROUP BY track, title',
                   (artist, album))
    return cursor.fetchall()

  @transaction
  def GetTrackPath(self, artist, album, title):
    for row in self.db.execute('SELECT DISTINCT path FROM Files WHERE artist = ? AND album = ? AND title = ?', (artist, album, title)):
      return row[0]
    else:
      return None

  def GetStats(self):
    stats = {}

    for row in self.db.execute('SELECT COUNT(*), SUM(size) FROM Files'):
      stats['files'] = row[0]
      stats['bytes'] = row[1]

    for row in self.db.execute('SELECT COUNT(*), SUM(size) FROM (SELECT MAX(size) AS size FROM Files'
                               ' GROUP BY artist, album, track, title)'):
      stats['unique_files'] = row[0]
      stats['unique_bytes'] = row[1]

    for row in self.db.execute('SELECT COUNT(*) FROM (SELECT DISTINCT artist FROM Files)'):
      stats['artists'] = row[0]

    for row in self.db.execute('SELECT COUNT(*) FROM (SELECT DISTINCT artist, album FROM Files GROUP BY artist, album)'):
      stats['albums'] = row[0]

    return stats

  @transaction
  def NeedsUpdate(self, path):
    try:
      st = os.stat(path)
    except OSError:
      return False
    for row in self.db.execute('SELECT updated FROM Files WHERE path = ?', (path,)):
      if row[0] < st.st_mtime:
        return True
      else:
        return False
    else:
      return True


def DecodeTagValue(value):
  """Attempt to decode a value returned from a tagger into unicode."""
  if isinstance(value, str):
    for encoding in SUPPORTED_TAG_ENCODINGS:
      try:
        return value.decode('utf-8')
      except UnicodeDecodeError:
        pass
    else:
      raise UnicodeDecodeError('cannot decode tag value: %r' % value)
  else:
    return value


def DecodeTrackValue(value):
  """Attempt to decode a track number."""
  match = TRACK_PATTERN.match(value)
  if match:
    return int(match.group(0))
  else:
    return None


def TranslateAacTag(tag):
  result = {}
  for aac_name, norm_name in AAC_FIELDS.iteritems():
    if aac_name in tag:
      value = tag[aac_name][0]
      if norm_name == 'tracknumber':
        if isinstance(value, tuple):
          value = str(value[0])
      result[norm_name] = [value]
  return result


def ProcessFile(path):
  """Attempt to obtain a tag from a file."""
  for tagger_name, tagger in TAGGERS.iteritems():
    try:
      tag = tagger.Open(path)
      break
    except:
      pass
  else:
    return None

  if tagger_name == 'aac':
    tag = TranslateAacTag(tag)

  fields = dict(media_type=tagger_name)
  for field, tag_name in TAGS.iteritems():
    if tag_name in tag and tag[tag_name]:
      value = DecodeTagValue(tag[tag_name][0])
      if field == 'track':
        value = DecodeTrackValue(value)
      fields[field] = value

  st = os.stat(path)
  fields['size'] = st.st_size

  return fields


class AddDirectoryJob(object):

  def __init__(self, source, path):
    self.source = source
    self.path = path

  def __call__(self, driver):
    driver.disk_watcher.AddSource(self.source, self.path)
    for dirpath, dirnames, filenames in os.walk(self.path):
      if driver.stopped:
        return
      driver.AddJob(AddFilesJob(self.source, dirpath, filenames))


class AddFilesJob(object):

  def __init__(self, source, dirpath, filenames):
    self.source = source
    self.dirpath = dirpath
    self.filenames = filenames

  def __call__(self, driver):
    inputs = {}
    for filename in self.filenames:
      if driver.stopped:
        return
      child_path = os.path.join(self.dirpath, filename)
      if driver.music.NeedsUpdate(child_path):
        fields = ProcessFile(child_path)
        logging.debug('scanned file: %s -> %s', child_path, fields)
        if fields:
          inputs[child_path] = fields
    for child_path, fields in inputs.iteritems():
      if driver.stopped:
        return
      driver.music.CheckFileWithinTransaction(child_path, fields)
      driver.music.db.commit()


class RemoveFilesJob(object):

  def __init__(self, source, path):
    self.source = source
    self.path = path

  def __call__(self, driver):
    count = driver.music.RemoveFilesUnder(self.source, self.path)
    logging.info('REMOVED %d FILES UNDER [%s] %s', count, self.source, self.path)


class ScanDriver(threading.Thread):

  def run(self):
    self.disk_watcher = DiskWatcher(self)
    self.disk_watcher.start()
    self.music = GetMusic()
    self.stopped = False
    self.queue = Queue.Queue()
    for source, path in self.music.GetSources():
      self.AddJob(AddDirectoryJob(source, path))
    logging.info('Starting scanner loop')
    while not self.stopped:
      job = self.GetJob()
      if not self.stopped and job:
        try:
          job(self)
        except:
          logging.exception('job threw exception')

  def GetJob(self):
    try:
      return self.queue.get(True, 1.0)
    except Queue.Empty:
      return None

  def AddJob(self, job):
    self.queue.put(job)

  def Stop(self):
    self.disk_watcher.Stop()
    self.stopped = True
    self.join()


class DiskWatcher(threading.Thread, pyinotify.ProcessEvent):

  def __init__(self, scanner):
    threading.Thread.__init__(self)
    self.scanner = scanner
    self.watch_manager = pyinotify.WatchManager()
    self.notifier = pyinotify.Notifier(self.watch_manager, self)
    self.watch_descriptors = {}
    self.wd_sources = {}
    self.stopped = False

  def Stop(self):
    self.stopped = True

  def AddSource(self, source, path):
    path = os.path.abspath(path)
    wdd = self.watch_manager.add_watch(path, pyinotify.EventsCodes.ALL_EVENTS,
                                       rec=True, auto_add=True)
    for path, wd in wdd.iteritems():
      logging.info('watching dir [%s]: %s', source, path)
      self.wd_sources[wd] = source
    self.watch_descriptors[source] = wdd

  def run(self):
    while not self.stopped:
      try:
        self.notifier.process_events()
        if self.notifier.check_events():
          self.notifier.read_events()
      except:
        logging.exception('inotify event processing threw exception')
    self.notifier.stop()

  def process_IN_CREATE(self, event):
    self.ProcessCreateEvent(event)

  def process_IN_DELETE(self, event):
    self.ProcessDeleteEvent(event)

  def process_IN_MOVED_TO(self, event):
    self.ProcessCreateEvent(event)

  def process_IN_MOVED_FROM(self, event):
    self.ProcessDeleteEvent(event)

  def process_IN_CLOSE_WRITE(self, event):
    source = self.wd_sources.get(event.wd)
    if source and not os.path.isdir(os.path.join(event.path, event.name)):
      self.scanner.AddJob(AddFilesJob(source, event.path, [event.name]))

  def ProcessCreateEvent(self, event):
    source = self.wd_sources.get(event.wd)
    if source:
      self.AddPath(source, os.path.join(event.path, event.name))

  def AddPath(self, source, path):
    logging.info('New item [%s]: %s', source, path)
    if os.path.isdir(path):
      self.scanner.AddJob(AddDirectoryJob(source, path))
    else:
      self.scanner.AddJob(
          AddFilesJob(source, os.path.dirname(path), [os.path.basename(path)]))

  def ProcessDeleteEvent(self, event):
    source = self.wd_sources.get(event.wd)
    if source:
      self.scanner.AddJob(RemoveFilesJob(source, os.path.join(event.path, event.name)))


if __name__ == '__main__':
  # TODO: make configurable
  logging_dir = os.path.join(os.getenv('HOME', '/'), '.mfuser')
  logging_path = os.path.join(logging_dir, 'mfuser.log')
  if not os.path.exists(logging_dir):
    os.makedirs(logging_dir)
  logging.basicConfig(level=logging.DEBUG,
                      format='%(asctime)s %(levelname)s %(message)s',
                      filename=logging_path,
                      filemode='w')
  usage = 'Music filesystem: Aggregate your music into a single organized index.'
  server = MusicFS(version='%prog ' + fuse.__version__, usage=usage)
  server.parse(errex=1)
  server.main()
